]> dgit.raspbian.org Git - gnome-shell.git/commitdiff
tests: Stop bundling "perf" tests with gnome-shell
authorFlorian Müllner <fmuellner@gnome.org>
Tue, 27 Jun 2023 18:27:53 +0000 (20:27 +0200)
committerFlorian Müllner <fmuellner@gnome.org>
Fri, 7 Jul 2023 17:43:28 +0000 (19:43 +0200)
Now that scripts are loaded as external modules, there's no reason
anymore for bundling them with the gnome-shell executable. Just
move the scripts into a dedicated folder in tests/ and run them
from there.

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2812>

12 files changed:
js/js-resources.gresource.xml
js/perf/basic.js [deleted file]
js/perf/closeWithActiveWindows.js [deleted file]
js/perf/core.js [deleted file]
js/perf/headlessStart.js [deleted file]
js/perf/hwtest.js [deleted file]
tests/meson.build
tests/shell/basic.js [new file with mode: 0644]
tests/shell/closeWithActiveWindows.js [new file with mode: 0644]
tests/shell/core.js [new file with mode: 0644]
tests/shell/headlessStart.js [new file with mode: 0644]
tests/shell/hwtest.js [new file with mode: 0644]

index c6e47915003884fdfb5b4ffc5ee2de22562f7efb..9c2c8881f4ea6baf0637706044345540957723af 100644 (file)
     <file>misc/util.js</file>
     <file>misc/weather.js</file>
 
-    <file>perf/basic.js</file>
-    <file>perf/closeWithActiveWindows.js</file>
-    <file>perf/core.js</file>
-    <file>perf/headlessStart.js</file>
-    <file>perf/hwtest.js</file>
-
     <file>ui/accessDialog.js</file>
     <file>ui/altTab.js</file>
     <file>ui/animation.js</file>
diff --git a/js/perf/basic.js b/js/perf/basic.js
deleted file mode 100644 (file)
index ef75ca4..0000000
+++ /dev/null
@@ -1,156 +0,0 @@
-// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
-/* eslint camelcase: ["error", { properties: "never", allow: ["^script_"] }] */
-
-const St = imports.gi.St;
-
-const Main = imports.ui.main;
-const MessageTray = imports.ui.messageTray;
-const Scripting = imports.ui.scripting;
-
-// This script tests the most important (basic) functionality of the shell.
-
-export var METRICS = {};
-
-export async function run() {
-    console.debug('Running basic perf test');
-
-    /* eslint-disable no-await-in-loop */
-    Scripting.defineScriptEvent('topBarNavStart', 'Starting to navigate the top bar');
-    Scripting.defineScriptEvent('topBarNavDone', 'Done navigating the top bar');
-    Scripting.defineScriptEvent('notificationShowStart', 'Showing a notification');
-    Scripting.defineScriptEvent('notificationShowDone', 'Done showing a notification');
-    Scripting.defineScriptEvent('notificationCloseStart', 'Closing a notification');
-    Scripting.defineScriptEvent('notificationCloseDone', 'Done closing a notification');
-    Scripting.defineScriptEvent('overviewShowStart', 'Starting to show the overview');
-    Scripting.defineScriptEvent('overviewShowDone', 'Overview finished showing');
-    Scripting.defineScriptEvent('applicationsShowStart', 'Starting to switch to applications view');
-    Scripting.defineScriptEvent('applicationsShowDone', 'Done switching to applications view');
-
-    Main.overview.connect('shown',
-        () => Scripting.scriptEvent('overviewShowDone'));
-
-    await Scripting.sleep(1000);
-
-    // navigate through top bar
-
-    console.debug('Navigate through top bar');
-    Scripting.scriptEvent('topBarNavStart');
-    Main.panel.statusArea.quickSettings.menu.open();
-    await Scripting.sleep(400);
-
-    const { menuManager } = Main.panel;
-    while (menuManager.activeMenu &&
-        Main.panel.navigate_focus(menuManager.activeMenu.sourceActor,
-            St.DirectionType.TAB_BACKWARD, false))
-        await Scripting.sleep(400);
-    Scripting.scriptEvent('topBarNavDone');
-
-    await Scripting.sleep(1000);
-
-    // notification
-    console.debug('Show notification message');
-    const source = new MessageTray.SystemNotificationSource();
-    Main.messageTray.add(source);
-
-    Scripting.scriptEvent('notificationShowStart');
-    source.connect('notification-show',
-        () => Scripting.scriptEvent('notificationShowDone'));
-
-    const notification = new MessageTray.Notification(source,
-        'A test notification');
-    source.showNotification(notification);
-    await Scripting.sleep(400);
-
-    console.debug('Show date menu');
-    Main.panel.statusArea.dateMenu.menu.open();
-    await Scripting.sleep(400);
-
-    Scripting.scriptEvent('notificationCloseStart');
-    notification.connect('destroy',
-        () => Scripting.scriptEvent('notificationCloseDone'));
-
-    console.debug('Destroy notification message');
-    notification.destroy();
-    await Scripting.sleep(400);
-
-    console.debug('Close date menu');
-    Main.panel.statusArea.dateMenu.menu.close();
-    await Scripting.waitLeisure();
-
-    await Scripting.sleep(1000);
-
-    // overview (window picker)
-    Scripting.scriptEvent('overviewShowStart');
-    Main.overview.show();
-    await Scripting.waitLeisure();
-    Main.overview.hide();
-    await Scripting.waitLeisure();
-
-    await Scripting.sleep(1000);
-
-    // overview (app picker)
-    console.debug('Showing overview');
-    Main.overview.show();
-    await Scripting.waitLeisure();
-
-    Scripting.scriptEvent('applicationsShowStart');
-    console.debug('Showing applications');
-    // eslint-disable-next-line require-atomic-updates
-    Main.overview.dash.showAppsButton.checked = true;
-    await Scripting.waitLeisure();
-    Scripting.scriptEvent('applicationsShowDone');
-    console.debug('Hiding applications');
-    // eslint-disable-next-line require-atomic-updates
-    Main.overview.dash.showAppsButton.checked = false;
-    await Scripting.waitLeisure();
-
-    console.debug('Hiding overview');
-    Main.overview.hide();
-    await Scripting.waitLeisure();
-    /* eslint-enable no-await-in-loop */
-
-    console.debug('Finished basic perf test');
-}
-
-let topBarNav = false;
-let notificationShown = false;
-let notificationClosed = false;
-let windowPickerShown = false;
-let appPickerShown = false;
-
-export function script_topBarNavDone() {
-    topBarNav = true;
-}
-
-export function script_notificationShowDone() {
-    notificationShown = true;
-}
-
-export function script_notificationCloseDone() {
-    notificationClosed = true;
-}
-
-export function script_overviewShowDone() {
-    windowPickerShown = true;
-}
-
-export function script_applicationsShowDone() {
-    appPickerShown = true;
-}
-
-export function finish() {
-    if (!topBarNav)
-        throw new Error('Failed to navigate top bar');
-
-    if (!notificationShown)
-        throw new Error('Failed to show notification');
-
-    if (!notificationClosed)
-        throw new Error('Failed to close notification');
-
-    if (!windowPickerShown)
-        throw new Error('Failed to show window picker');
-
-    if (!appPickerShown)
-        throw new Error('Failed to show app picker');
-}
diff --git a/js/perf/closeWithActiveWindows.js b/js/perf/closeWithActiveWindows.js
deleted file mode 100644 (file)
index 2d69679..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
-/* eslint camelcase: ["error", { properties: "never", allow: ["^script_", "^malloc", "^glx", "^clutter"] }] */
-
-const Clutter = imports.gi.Clutter;
-
-const Main = imports.ui.main;
-const Scripting = imports.ui.scripting;
-
-/** Run test. */
-export async function run() {
-    /* eslint-disable no-await-in-loop */
-
-    /* Make created windows remain visible during exit. */
-    await Scripting.disableHelperAutoExit();
-
-    const seat = Clutter.get_default_backend().get_default_seat();
-    const virtualDevice_ =
-        seat.create_virtual_device(Clutter.InputDeviceType.KEYBOARD_DEVICE);
-
-    Main.overview.hide();
-    await Scripting.waitLeisure();
-    await Scripting.sleep(1000);
-
-    await Scripting.createTestWindow({
-        width: 640,
-        height: 480,
-        textInput: true,
-    });
-
-    await Scripting.waitTestWindows();
-    await Scripting.waitLeisure();
-    await Scripting.sleep(1000);
-
-    /* eslint-enable no-await-in-loop */
-}
diff --git a/js/perf/core.js b/js/perf/core.js
deleted file mode 100644 (file)
index 78d17d8..0000000
+++ /dev/null
@@ -1,267 +0,0 @@
-// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
-/* eslint camelcase: ["error", { properties: "never", allow: ["^script_", "^malloc", "^glx", "^clutter"] }] */
-
-const System = imports.system;
-
-const Main = imports.ui.main;
-const Scripting = imports.ui.scripting;
-
-// This performance script measure the most important (core) performance
-// metrics for the shell. By looking at the output metrics of this script
-// someone should be able to get an idea of how well the shell is performing
-// on a particular system.
-
-export var METRICS = {
-    overviewLatencyFirst: {
-        description: 'Time to first frame after triggering overview, first time',
-        units: 'us',
-    },
-    overviewFpsFirst: {
-        description: 'Frame rate when going to the overview, first time',
-        units: 'frames / s',
-    },
-    overviewLatencySubsequent: {
-        description: 'Time to first frame after triggering overview, second time',
-        units: 'us',
-    },
-    overviewFpsSubsequent: {
-        description: 'Frames rate when going to the overview, second time',
-        units: 'frames / s',
-    },
-    overviewFps5Windows: {
-        description: 'Frames rate when going to the overview, 5 windows open',
-        units: 'frames / s',
-    },
-    overviewFps10Windows: {
-        description: 'Frames rate when going to the overview, 10 windows open',
-        units: 'frames / s',
-    },
-    overviewFps5Maximized: {
-        description: 'Frames rate when going to the overview, 5 maximized windows open',
-        units: 'frames / s',
-    },
-    overviewFps10Maximized: {
-        description: 'Frames rate when going to the overview, 10 maximized windows open',
-        units: 'frames / s',
-    },
-    overviewFps5Alpha: {
-        description: 'Frames rate when going to the overview, 5 alpha-transparent windows open',
-        units: 'frames / s',
-    },
-    overviewFps10Alpha: {
-        description: 'Frames rate when going to the overview, 10 alpha-transparent windows open',
-        units: 'frames / s',
-    },
-    usedAfterOverview: {
-        description: "Malloc'ed bytes after the overview is shown once",
-        units: 'B',
-    },
-    leakedAfterOverview: {
-        description: "Additional malloc'ed bytes the second time the overview is shown",
-        units: 'B',
-    },
-    applicationsShowTimeFirst: {
-        description: 'Time to switch to applications view, first time',
-        units: 'us',
-    },
-    applicationsShowTimeSubsequent: {
-        description: 'Time to switch to applications view, second time',
-        units: 'us',
-    },
-};
-
-const WINDOW_CONFIGS = [{
-    width: 640, height: 480,
-    alpha: false, maximized: false, count: 1,  metric: 'overviewFpsSubsequent',
-}, {
-    width: 640, height: 480,
-    alpha: false, maximized: false, count: 5,  metric: 'overviewFps5Windows',
-}, {
-    width: 640, height: 480,
-    alpha: false, maximized: false, count: 10, metric: 'overviewFps10Windows',
-}, {
-    width: 640, height: 480,
-    alpha: false, maximized: true,  count: 5,  metric: 'overviewFps5Maximized',
-}, {
-    width: 640, height: 480,
-    alpha: false, maximized: true,  count: 10, metric: 'overviewFps10Maximized',
-}, {
-    width: 640, height: 480,
-    alpha: true,  maximized: false, count: 5,  metric: 'overviewFps5Alpha',
-}, {
-    width: 640, height: 480,
-    alpha: true,  maximized: false, count: 10, metric: 'overviewFps10Alpha',
-}];
-
-export async function run() {
-    /* eslint-disable no-await-in-loop */
-    Scripting.defineScriptEvent("overviewShowStart", "Starting to show the overview");
-    Scripting.defineScriptEvent("overviewShowDone", "Overview finished showing");
-    Scripting.defineScriptEvent("afterShowHide", "After a show/hide cycle for the overview");
-    Scripting.defineScriptEvent("applicationsShowStart", "Starting to switch to applications view");
-    Scripting.defineScriptEvent("applicationsShowDone", "Done switching to applications view");
-
-    // Enable recording of timestamps for different points in the frame cycle
-    global.frame_timestamps = true;
-
-    Main.overview.connect('shown', () => {
-        Scripting.scriptEvent('overviewShowDone');
-    });
-
-    await Scripting.sleep(1000);
-
-    for (let i = 0; i < 2 * WINDOW_CONFIGS.length; i++) {
-        // We go to the overview twice for each configuration; the first time
-        // to calculate the mipmaps for the windows, the second time to get
-        // a clean set of numbers.
-        if ((i % 2) == 0) {
-            let config = WINDOW_CONFIGS[i / 2];
-            await Scripting.destroyTestWindows();
-
-            for (let k = 0; k < config.count; k++) {
-                await Scripting.createTestWindow({
-                    width: config.width,
-                    height: config.height,
-                    alpha: config.alpha,
-                    maximized: config.maximized,
-                });
-            }
-
-            await Scripting.waitTestWindows();
-            await Scripting.sleep(1000);
-            await Scripting.waitLeisure();
-        }
-
-        Scripting.scriptEvent('overviewShowStart');
-        Main.overview.show();
-
-        await Scripting.waitLeisure();
-        Main.overview.hide();
-        await Scripting.waitLeisure();
-
-        System.gc();
-        await Scripting.sleep(1000);
-        Scripting.collectStatistics();
-        Scripting.scriptEvent('afterShowHide');
-    }
-
-    await Scripting.destroyTestWindows();
-    await Scripting.sleep(1000);
-
-    Main.overview.show();
-    await Scripting.waitLeisure();
-
-    for (let i = 0; i < 2; i++) {
-        Scripting.scriptEvent('applicationsShowStart');
-        // eslint-disable-next-line require-atomic-updates
-        Main.overview.dash.showAppsButton.checked = true;
-        await Scripting.waitLeisure();
-        Scripting.scriptEvent('applicationsShowDone');
-        // eslint-disable-next-line require-atomic-updates
-        Main.overview.dash.showAppsButton.checked = false;
-        await Scripting.waitLeisure();
-    }
-    /* eslint-enable no-await-in-loop */
-}
-
-let showingOverview = false;
-let finishedShowingOverview = false;
-let overviewShowStart;
-let overviewFrames;
-let overviewLatency;
-let mallocUsedSize = 0;
-let overviewShowCount = 0;
-let haveSwapComplete = false;
-let applicationsShowStart;
-let applicationsShowCount = 0;
-
-export function script_overviewShowStart(time) {
-    showingOverview = true;
-    finishedShowingOverview = false;
-    overviewShowStart = time;
-    overviewFrames = 0;
-}
-
-export function script_overviewShowDone(_time) {
-    // We've set up the state at the end of the zoom out, but we
-    // need to wait for one more frame to paint before we count
-    // ourselves as done.
-    finishedShowingOverview = true;
-}
-
-export function script_applicationsShowStart(time) {
-    applicationsShowStart = time;
-}
-
-export function script_applicationsShowDone(time) {
-    applicationsShowCount++;
-    if (applicationsShowCount == 1)
-        METRICS.applicationsShowTimeFirst.value = time - applicationsShowStart;
-    else
-        METRICS.applicationsShowTimeSubsequent.value = time - applicationsShowStart;
-}
-
-export function script_afterShowHide(_time) {
-    if (overviewShowCount == 1)
-        METRICS.usedAfterOverview.value = mallocUsedSize;
-    else
-        METRICS.leakedAfterOverview.value = mallocUsedSize - METRICS.usedAfterOverview.value;
-}
-
-export function malloc_usedSize(time, bytes) {
-    mallocUsedSize = bytes;
-}
-
-function _frameDone(time) {
-    if (showingOverview) {
-        if (overviewFrames == 0)
-            overviewLatency = time - overviewShowStart;
-
-        overviewFrames++;
-    }
-
-    if (finishedShowingOverview) {
-        showingOverview = false;
-        finishedShowingOverview = false;
-        overviewShowCount++;
-
-        let dt = (time - (overviewShowStart + overviewLatency)) / 1000000;
-
-        // If we see a start frame and an end frame, that would
-        // be 1 frame for a FPS computation, hence the '- 1'
-        let fps = (overviewFrames - 1) / dt;
-
-        if (overviewShowCount == 1) {
-            METRICS.overviewLatencyFirst.value = overviewLatency;
-            METRICS.overviewFpsFirst.value = fps;
-        } else if (overviewShowCount == 2) {
-            METRICS.overviewLatencySubsequent.value = overviewLatency;
-        }
-
-        // Other than overviewFpsFirst, we collect FPS metrics the second
-        // we show each window configuration. overviewShowCount is 1,2,3...
-        if (overviewShowCount % 2 == 0) {
-            let config = WINDOW_CONFIGS[(overviewShowCount / 2) - 1];
-            METRICS[config.metric].value = fps;
-        }
-    }
-}
-
-export function glx_swapComplete(time, swapTime) {
-    haveSwapComplete = true;
-
-    _frameDone(swapTime);
-}
-
-export function clutter_stagePaintDone(time) {
-    // If we aren't receiving GLXBufferSwapComplete events, then we approximate
-    // the time the user sees a frame with the time we finished doing drawing
-    // commands for the frame. This doesn't take into account the time for
-    // the GPU to finish painting, and the time for waiting for the buffer
-    // swap, but if this are uniform - every frame takes the same time to draw -
-    // then it won't upset our FPS calculation, though the latency value
-    // will be slightly too low.
-
-    if (!haveSwapComplete)
-        _frameDone(time);
-}
diff --git a/js/perf/headlessStart.js b/js/perf/headlessStart.js
deleted file mode 100644 (file)
index 6f4157b..0000000
+++ /dev/null
@@ -1,107 +0,0 @@
-// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
-/* eslint camelcase: ["error", { properties: "never", allow: ["^script_"] }] */
-
-const GLib = imports.gi.GLib;
-const MetaTest = imports.gi.MetaTest;
-const Shell = imports.gi.Shell;
-
-const Main = imports.ui.main;
-const Scripting = imports.ui.scripting;
-
-// This script tests that the shell handles connecting monitors after startup
-// is properly handled.
-
-export var METRICS = {};
-
-let _testMonitor = null;
-
-/**
- * init:
- */
-export function init() {
-    global.connect('shutdown', () => {
-        _testMonitor?.destroy();
-    });
-    GLib.timeout_add_seconds(
-        GLib.PRIORITY_LOW, 2,
-        () => {
-            log('Connecting 1280x720 test monitor');
-            _testMonitor = MetaTest.TestMonitor.new(
-                global.context, 1280, 720, 60.0);
-        });
-    Scripting.defineScriptEvent('monitorsChanged', 'Monitors changed');
-
-    const display = global.display;
-    console.assert(display.get_n_monitors() === 0, 'Not running headless');
-
-    const monitorManager = global.backend.get_monitor_manager();
-    const monitorsChangedHandlerId = monitorManager.connect('monitors-changed',
-        () => {
-            console.assert(display.get_n_monitors() === 1,
-                'Expected a single monitor');
-            Scripting.scriptEvent('monitorsChanged');
-            monitorManager.disconnect(monitorsChangedHandlerId);
-        });
-    Shell.PerfLog.get_default().set_enabled(true);
-}
-
-/**
- * run:
- */
-export async function run() {
-    /* eslint-disable no-await-in-loop */
-    Scripting.defineScriptEvent('overviewShowDone', 'Overview finished showing');
-    Scripting.defineScriptEvent('overviewHideDone', 'Overview finished hiding');
-
-    Main.overview.connect('shown',
-        () => Scripting.scriptEvent('overviewShowDone'));
-    Main.overview.connect('hidden',
-        () => Scripting.scriptEvent('overviewHideDone'));
-
-    Main.overview.hide();
-    await Scripting.waitLeisure();
-
-    Main.overview.show();
-    await Scripting.waitLeisure();
-
-    /* eslint-enable no-await-in-loop */
-}
-
-let monitorsChanged = false;
-let overviewHidden = false;
-let overviewShown = false;
-
-/**
- * script_monitorsChanged:
- */
-export function script_monitorsChanged() {
-    monitorsChanged = true;
-}
-
-/**
- * script_overviewHideDone:
- */
-export function script_overviewHideDone() {
-    overviewHidden = true;
-}
-
-/**
- * script_overviewShowDone:
- */
-export function script_overviewShowDone() {
-    overviewShown = true;
-}
-
-/**
- * finish:
- */
-export function finish() {
-    if (!monitorsChanged)
-        throw new Error('Monitors never changed');
-
-    if (!overviewHidden)
-        throw new Error('Failed to hide overview');
-
-    if (!overviewShown)
-        throw new Error('Failed to show overview');
-}
diff --git a/js/perf/hwtest.js b/js/perf/hwtest.js
deleted file mode 100644 (file)
index 6e4cd7e..0000000
+++ /dev/null
@@ -1,313 +0,0 @@
-/* eslint camelcase: ["error", { properties: "never", allow: ["^script_", "^clutter"] }] */
-const Clutter = imports.gi.Clutter;
-const Gio = imports.gi.Gio;
-const Shell = imports.gi.Shell;
-const Main = imports.ui.main;
-const Scripting = imports.ui.scripting;
-
-export var METRICS = {
-    timeToDesktop: {
-        description: 'Time from starting graphical.target to desktop showing',
-        units: 'us',
-    },
-
-    overviewShowTime: {
-        description: 'Time to switch to overview view, first time',
-        units: 'us',
-    },
-
-    applicationsShowTime: {
-        description: 'Time to switch to applications view, first time',
-        units: 'us',
-    },
-
-    mainViewRedrawTime: {
-        description: 'Time to redraw the main view, full screen',
-        units: 'us',
-    },
-
-    overviewRedrawTime: {
-        description: 'Time to redraw the overview, full screen, 5 windows',
-        units: 'us',
-    },
-
-    applicationRedrawTime: {
-        description: 'Time to redraw frame with a maximized application update',
-        units: 'us',
-    },
-
-    geditStartTime: {
-        description: 'Time from gedit launch to window drawn',
-        units: 'us',
-    },
-};
-
-function waitAndDraw(milliseconds) {
-    let cb;
-
-    let timeline = new Clutter.Timeline({ duration: milliseconds });
-    timeline.start();
-
-    timeline.connect('new-frame', (_timeline, _frame) => {
-        global.stage.queue_redraw();
-    });
-
-    timeline.connect('completed', () => {
-        timeline.stop();
-        if (cb)
-            cb();
-    });
-
-    return callback => (cb = callback);
-}
-
-function waitSignal(object, signal) {
-    let cb;
-
-    let id = object.connect(signal, () => {
-        object.disconnect(id);
-        if (cb)
-            cb();
-    });
-
-    return callback => (cb = callback);
-}
-
-function extractBootTimestamp() {
-    const sp = Gio.Subprocess.new([
-        'journalctl', '-b',
-        'MESSAGE_ID=7d4958e842da4a758f6c1cdc7b36dcc5',
-        'UNIT=graphical.target',
-        '-o',
-        'json',
-    ], Gio.SubprocessFlags.STDOUT_PIPE);
-    let result = null;
-
-    let datastream = Gio.DataInputStream.new(sp.get_stdout_pipe());
-    while (true) { // eslint-disable-line no-constant-condition
-        let [line, length_] = datastream.read_line_utf8(null);
-        if (line === null)
-            break;
-
-        let fields = JSON.parse(line);
-        result = Number(fields['__MONOTONIC_TIMESTAMP']);
-    }
-    datastream.close(null);
-    return result;
-}
-
-export async function run() {
-    /* eslint-disable no-await-in-loop */
-    Scripting.defineScriptEvent("desktopShown", "Finished initial animation");
-    Scripting.defineScriptEvent("overviewShowStart", "Starting to show the overview");
-    Scripting.defineScriptEvent("overviewShowDone", "Overview finished showing");
-    Scripting.defineScriptEvent("applicationsShowStart", "Starting to switch to applications view");
-    Scripting.defineScriptEvent("applicationsShowDone", "Done switching to applications view");
-    Scripting.defineScriptEvent("mainViewDrawStart", "Drawing main view");
-    Scripting.defineScriptEvent("mainViewDrawDone", "Ending timing main view drawing");
-    Scripting.defineScriptEvent("overviewDrawStart", "Drawing overview");
-    Scripting.defineScriptEvent("overviewDrawDone", "Ending timing overview drawing");
-    Scripting.defineScriptEvent("redrawTestStart", "Drawing application window");
-    Scripting.defineScriptEvent("redrawTestDone", "Ending timing application window drawing");
-    Scripting.defineScriptEvent("collectTimings", "Accumulate frame timings from redraw tests");
-    Scripting.defineScriptEvent("geditLaunch", "gedit application launch");
-    Scripting.defineScriptEvent("geditFirstFrame", "first frame of gedit window drawn");
-
-    await Scripting.waitLeisure();
-    Scripting.scriptEvent('desktopShown');
-
-    let interfaceSettings = new Gio.Settings({
-        schema_id: 'org.gnome.desktop.interface',
-    });
-    interfaceSettings.set_boolean('enable-animations', false);
-
-    Scripting.scriptEvent('overviewShowStart');
-    Main.overview.show();
-    await Scripting.waitLeisure();
-    Scripting.scriptEvent('overviewShowDone');
-
-    await Scripting.sleep(1000);
-
-    Scripting.scriptEvent('applicationsShowStart');
-    // eslint-disable-next-line require-atomic-updates
-    Main.overview.dash.showAppsButton.checked = true;
-
-    await Scripting.waitLeisure();
-    Scripting.scriptEvent('applicationsShowDone');
-
-    await Scripting.sleep(1000);
-
-    Main.overview.hide();
-    await Scripting.waitLeisure();
-
-    // --------------------- //
-    // Tests of redraw speed //
-    // --------------------- //
-
-    global.frame_timestamps = true;
-    global.frame_finish_timestamp = true;
-
-    for (let k = 0; k < 5; k++)
-        await Scripting.createTestWindow({ maximized: true });
-    await Scripting.waitTestWindows();
-
-    await Scripting.sleep(1000);
-
-    Scripting.scriptEvent('mainViewDrawStart');
-    await waitAndDraw(1000);
-    Scripting.scriptEvent('mainViewDrawDone');
-
-    Main.overview.show();
-    Scripting.waitLeisure();
-
-    await Scripting.sleep(1500);
-
-    Scripting.scriptEvent('overviewDrawStart');
-    await waitAndDraw(1000);
-    Scripting.scriptEvent('overviewDrawDone');
-
-    await Scripting.destroyTestWindows();
-    Main.overview.hide();
-
-    await Scripting.createTestWindow({
-        maximized: true,
-        redraws: true,
-    });
-    await Scripting.waitTestWindows();
-
-    await Scripting.sleep(1000);
-
-    Scripting.scriptEvent('redrawTestStart');
-    await Scripting.sleep(1000);
-    Scripting.scriptEvent('redrawTestDone');
-
-    await Scripting.sleep(1000);
-    Scripting.scriptEvent('collectTimings');
-
-    await Scripting.destroyTestWindows();
-
-    global.frame_timestamps = false;
-    global.frame_finish_timestamp = false;
-
-    await Scripting.sleep(1000);
-
-    let appSys = Shell.AppSystem.get_default();
-    let app = appSys.lookup_app('org.gnome.gedit.desktop');
-
-    Scripting.scriptEvent('geditLaunch');
-    app.activate();
-
-    let windows = app.get_windows();
-    if (windows.length > 0)
-        throw new Error('gedit was already running');
-
-    while (windows.length == 0) {
-        await waitSignal(global.display, 'window-created');
-        windows = app.get_windows();
-    }
-
-    let actor = windows[0].get_compositor_private();
-    await waitSignal(actor, 'first-frame');
-    Scripting.scriptEvent('geditFirstFrame');
-
-    await Scripting.sleep(1000);
-
-    windows[0].delete(global.get_current_time());
-
-    await Scripting.sleep(1000);
-
-    interfaceSettings.set_boolean('enable-animations', true);
-    /* eslint-enable no-await-in-loop */
-}
-
-let overviewShowStart;
-let applicationsShowStart;
-let stagePaintStart;
-let redrawTiming;
-let redrawTimes = {};
-let geditLaunchTime;
-
-export function script_desktopShown(time) {
-    let bootTimestamp = extractBootTimestamp();
-    METRICS.timeToDesktop.value = time - bootTimestamp;
-}
-
-export function script_overviewShowStart(time) {
-    overviewShowStart = time;
-}
-
-export function script_overviewShowDone(time) {
-    METRICS.overviewShowTime.value = time - overviewShowStart;
-}
-
-export function script_applicationsShowStart(time) {
-    applicationsShowStart = time;
-}
-
-export function script_applicationsShowDone(time) {
-    METRICS.applicationsShowTime.value = time - applicationsShowStart;
-}
-
-export function script_mainViewDrawStart(_time) {
-    redrawTiming = 'mainView';
-}
-
-export function script_mainViewDrawDone(_time) {
-    redrawTiming = null;
-}
-
-export function script_overviewDrawStart(_time) {
-    redrawTiming = 'overview';
-}
-
-export function script_overviewDrawDone(_time) {
-    redrawTiming = null;
-}
-
-export function script_redrawTestStart(_time) {
-    redrawTiming = 'application';
-}
-
-export function script_redrawTestDone(_time) {
-    redrawTiming = null;
-}
-
-export function script_collectTimings(_time) {
-    for (let timing in redrawTimes) {
-        let times = redrawTimes[timing];
-        times.sort((a, b) => a - b);
-
-        let len = times.length;
-        let median;
-
-        if (len == 0)
-            median = -1;
-        else if (len % 2 == 1)
-            median = times[(len - 1) / 2];
-        else
-            median = Math.round((times[len / 2 - 1] + times[len / 2]) / 2);
-
-        METRICS[`${timing}RedrawTime`].value = median;
-    }
-}
-
-export function script_geditLaunch(time) {
-    geditLaunchTime = time;
-}
-
-export function script_geditFirstFrame(time) {
-    METRICS.geditStartTime.value = time - geditLaunchTime;
-}
-
-export function clutter_stagePaintStart(time) {
-    stagePaintStart = time;
-}
-
-export function clutter_paintCompletedTimestamp(time) {
-    if (redrawTiming != null && stagePaintStart != null) {
-        if (!(redrawTiming in redrawTimes))
-            redrawTimes[redrawTiming] = [];
-        redrawTimes[redrawTiming].push(time - stagePaintStart);
-    }
-    stagePaintStart = null;
-}
index 49cc3b1c2c1af0534c72463033c6b6ff774b9b07..d54bdbbbf06bb045e0d81a2f0a08e6ff5deee42c 100644 (file)
@@ -72,7 +72,7 @@ foreach shell_test : shell_tests
       test_tool,
       '--headless',
       options,
-      '@0@/js/perf/@1@.js'.format(meson.project_source_root(), test_name),
+      '@0@/shell/@1@.js'.format(meson.current_source_dir(), test_name),
     ],
     is_parallel: false,
     env: shell_testenv,
diff --git a/tests/shell/basic.js b/tests/shell/basic.js
new file mode 100644 (file)
index 0000000..ef75ca4
--- /dev/null
@@ -0,0 +1,156 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* eslint camelcase: ["error", { properties: "never", allow: ["^script_"] }] */
+
+const St = imports.gi.St;
+
+const Main = imports.ui.main;
+const MessageTray = imports.ui.messageTray;
+const Scripting = imports.ui.scripting;
+
+// This script tests the most important (basic) functionality of the shell.
+
+export var METRICS = {};
+
+export async function run() {
+    console.debug('Running basic perf test');
+
+    /* eslint-disable no-await-in-loop */
+    Scripting.defineScriptEvent('topBarNavStart', 'Starting to navigate the top bar');
+    Scripting.defineScriptEvent('topBarNavDone', 'Done navigating the top bar');
+    Scripting.defineScriptEvent('notificationShowStart', 'Showing a notification');
+    Scripting.defineScriptEvent('notificationShowDone', 'Done showing a notification');
+    Scripting.defineScriptEvent('notificationCloseStart', 'Closing a notification');
+    Scripting.defineScriptEvent('notificationCloseDone', 'Done closing a notification');
+    Scripting.defineScriptEvent('overviewShowStart', 'Starting to show the overview');
+    Scripting.defineScriptEvent('overviewShowDone', 'Overview finished showing');
+    Scripting.defineScriptEvent('applicationsShowStart', 'Starting to switch to applications view');
+    Scripting.defineScriptEvent('applicationsShowDone', 'Done switching to applications view');
+
+    Main.overview.connect('shown',
+        () => Scripting.scriptEvent('overviewShowDone'));
+
+    await Scripting.sleep(1000);
+
+    // navigate through top bar
+
+    console.debug('Navigate through top bar');
+    Scripting.scriptEvent('topBarNavStart');
+    Main.panel.statusArea.quickSettings.menu.open();
+    await Scripting.sleep(400);
+
+    const { menuManager } = Main.panel;
+    while (menuManager.activeMenu &&
+        Main.panel.navigate_focus(menuManager.activeMenu.sourceActor,
+            St.DirectionType.TAB_BACKWARD, false))
+        await Scripting.sleep(400);
+    Scripting.scriptEvent('topBarNavDone');
+
+    await Scripting.sleep(1000);
+
+    // notification
+    console.debug('Show notification message');
+    const source = new MessageTray.SystemNotificationSource();
+    Main.messageTray.add(source);
+
+    Scripting.scriptEvent('notificationShowStart');
+    source.connect('notification-show',
+        () => Scripting.scriptEvent('notificationShowDone'));
+
+    const notification = new MessageTray.Notification(source,
+        'A test notification');
+    source.showNotification(notification);
+    await Scripting.sleep(400);
+
+    console.debug('Show date menu');
+    Main.panel.statusArea.dateMenu.menu.open();
+    await Scripting.sleep(400);
+
+    Scripting.scriptEvent('notificationCloseStart');
+    notification.connect('destroy',
+        () => Scripting.scriptEvent('notificationCloseDone'));
+
+    console.debug('Destroy notification message');
+    notification.destroy();
+    await Scripting.sleep(400);
+
+    console.debug('Close date menu');
+    Main.panel.statusArea.dateMenu.menu.close();
+    await Scripting.waitLeisure();
+
+    await Scripting.sleep(1000);
+
+    // overview (window picker)
+    Scripting.scriptEvent('overviewShowStart');
+    Main.overview.show();
+    await Scripting.waitLeisure();
+    Main.overview.hide();
+    await Scripting.waitLeisure();
+
+    await Scripting.sleep(1000);
+
+    // overview (app picker)
+    console.debug('Showing overview');
+    Main.overview.show();
+    await Scripting.waitLeisure();
+
+    Scripting.scriptEvent('applicationsShowStart');
+    console.debug('Showing applications');
+    // eslint-disable-next-line require-atomic-updates
+    Main.overview.dash.showAppsButton.checked = true;
+    await Scripting.waitLeisure();
+    Scripting.scriptEvent('applicationsShowDone');
+    console.debug('Hiding applications');
+    // eslint-disable-next-line require-atomic-updates
+    Main.overview.dash.showAppsButton.checked = false;
+    await Scripting.waitLeisure();
+
+    console.debug('Hiding overview');
+    Main.overview.hide();
+    await Scripting.waitLeisure();
+    /* eslint-enable no-await-in-loop */
+
+    console.debug('Finished basic perf test');
+}
+
+let topBarNav = false;
+let notificationShown = false;
+let notificationClosed = false;
+let windowPickerShown = false;
+let appPickerShown = false;
+
+export function script_topBarNavDone() {
+    topBarNav = true;
+}
+
+export function script_notificationShowDone() {
+    notificationShown = true;
+}
+
+export function script_notificationCloseDone() {
+    notificationClosed = true;
+}
+
+export function script_overviewShowDone() {
+    windowPickerShown = true;
+}
+
+export function script_applicationsShowDone() {
+    appPickerShown = true;
+}
+
+export function finish() {
+    if (!topBarNav)
+        throw new Error('Failed to navigate top bar');
+
+    if (!notificationShown)
+        throw new Error('Failed to show notification');
+
+    if (!notificationClosed)
+        throw new Error('Failed to close notification');
+
+    if (!windowPickerShown)
+        throw new Error('Failed to show window picker');
+
+    if (!appPickerShown)
+        throw new Error('Failed to show app picker');
+}
diff --git a/tests/shell/closeWithActiveWindows.js b/tests/shell/closeWithActiveWindows.js
new file mode 100644 (file)
index 0000000..2d69679
--- /dev/null
@@ -0,0 +1,35 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* eslint camelcase: ["error", { properties: "never", allow: ["^script_", "^malloc", "^glx", "^clutter"] }] */
+
+const Clutter = imports.gi.Clutter;
+
+const Main = imports.ui.main;
+const Scripting = imports.ui.scripting;
+
+/** Run test. */
+export async function run() {
+    /* eslint-disable no-await-in-loop */
+
+    /* Make created windows remain visible during exit. */
+    await Scripting.disableHelperAutoExit();
+
+    const seat = Clutter.get_default_backend().get_default_seat();
+    const virtualDevice_ =
+        seat.create_virtual_device(Clutter.InputDeviceType.KEYBOARD_DEVICE);
+
+    Main.overview.hide();
+    await Scripting.waitLeisure();
+    await Scripting.sleep(1000);
+
+    await Scripting.createTestWindow({
+        width: 640,
+        height: 480,
+        textInput: true,
+    });
+
+    await Scripting.waitTestWindows();
+    await Scripting.waitLeisure();
+    await Scripting.sleep(1000);
+
+    /* eslint-enable no-await-in-loop */
+}
diff --git a/tests/shell/core.js b/tests/shell/core.js
new file mode 100644 (file)
index 0000000..78d17d8
--- /dev/null
@@ -0,0 +1,267 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* eslint camelcase: ["error", { properties: "never", allow: ["^script_", "^malloc", "^glx", "^clutter"] }] */
+
+const System = imports.system;
+
+const Main = imports.ui.main;
+const Scripting = imports.ui.scripting;
+
+// This performance script measure the most important (core) performance
+// metrics for the shell. By looking at the output metrics of this script
+// someone should be able to get an idea of how well the shell is performing
+// on a particular system.
+
+export var METRICS = {
+    overviewLatencyFirst: {
+        description: 'Time to first frame after triggering overview, first time',
+        units: 'us',
+    },
+    overviewFpsFirst: {
+        description: 'Frame rate when going to the overview, first time',
+        units: 'frames / s',
+    },
+    overviewLatencySubsequent: {
+        description: 'Time to first frame after triggering overview, second time',
+        units: 'us',
+    },
+    overviewFpsSubsequent: {
+        description: 'Frames rate when going to the overview, second time',
+        units: 'frames / s',
+    },
+    overviewFps5Windows: {
+        description: 'Frames rate when going to the overview, 5 windows open',
+        units: 'frames / s',
+    },
+    overviewFps10Windows: {
+        description: 'Frames rate when going to the overview, 10 windows open',
+        units: 'frames / s',
+    },
+    overviewFps5Maximized: {
+        description: 'Frames rate when going to the overview, 5 maximized windows open',
+        units: 'frames / s',
+    },
+    overviewFps10Maximized: {
+        description: 'Frames rate when going to the overview, 10 maximized windows open',
+        units: 'frames / s',
+    },
+    overviewFps5Alpha: {
+        description: 'Frames rate when going to the overview, 5 alpha-transparent windows open',
+        units: 'frames / s',
+    },
+    overviewFps10Alpha: {
+        description: 'Frames rate when going to the overview, 10 alpha-transparent windows open',
+        units: 'frames / s',
+    },
+    usedAfterOverview: {
+        description: "Malloc'ed bytes after the overview is shown once",
+        units: 'B',
+    },
+    leakedAfterOverview: {
+        description: "Additional malloc'ed bytes the second time the overview is shown",
+        units: 'B',
+    },
+    applicationsShowTimeFirst: {
+        description: 'Time to switch to applications view, first time',
+        units: 'us',
+    },
+    applicationsShowTimeSubsequent: {
+        description: 'Time to switch to applications view, second time',
+        units: 'us',
+    },
+};
+
+const WINDOW_CONFIGS = [{
+    width: 640, height: 480,
+    alpha: false, maximized: false, count: 1,  metric: 'overviewFpsSubsequent',
+}, {
+    width: 640, height: 480,
+    alpha: false, maximized: false, count: 5,  metric: 'overviewFps5Windows',
+}, {
+    width: 640, height: 480,
+    alpha: false, maximized: false, count: 10, metric: 'overviewFps10Windows',
+}, {
+    width: 640, height: 480,
+    alpha: false, maximized: true,  count: 5,  metric: 'overviewFps5Maximized',
+}, {
+    width: 640, height: 480,
+    alpha: false, maximized: true,  count: 10, metric: 'overviewFps10Maximized',
+}, {
+    width: 640, height: 480,
+    alpha: true,  maximized: false, count: 5,  metric: 'overviewFps5Alpha',
+}, {
+    width: 640, height: 480,
+    alpha: true,  maximized: false, count: 10, metric: 'overviewFps10Alpha',
+}];
+
+export async function run() {
+    /* eslint-disable no-await-in-loop */
+    Scripting.defineScriptEvent("overviewShowStart", "Starting to show the overview");
+    Scripting.defineScriptEvent("overviewShowDone", "Overview finished showing");
+    Scripting.defineScriptEvent("afterShowHide", "After a show/hide cycle for the overview");
+    Scripting.defineScriptEvent("applicationsShowStart", "Starting to switch to applications view");
+    Scripting.defineScriptEvent("applicationsShowDone", "Done switching to applications view");
+
+    // Enable recording of timestamps for different points in the frame cycle
+    global.frame_timestamps = true;
+
+    Main.overview.connect('shown', () => {
+        Scripting.scriptEvent('overviewShowDone');
+    });
+
+    await Scripting.sleep(1000);
+
+    for (let i = 0; i < 2 * WINDOW_CONFIGS.length; i++) {
+        // We go to the overview twice for each configuration; the first time
+        // to calculate the mipmaps for the windows, the second time to get
+        // a clean set of numbers.
+        if ((i % 2) == 0) {
+            let config = WINDOW_CONFIGS[i / 2];
+            await Scripting.destroyTestWindows();
+
+            for (let k = 0; k < config.count; k++) {
+                await Scripting.createTestWindow({
+                    width: config.width,
+                    height: config.height,
+                    alpha: config.alpha,
+                    maximized: config.maximized,
+                });
+            }
+
+            await Scripting.waitTestWindows();
+            await Scripting.sleep(1000);
+            await Scripting.waitLeisure();
+        }
+
+        Scripting.scriptEvent('overviewShowStart');
+        Main.overview.show();
+
+        await Scripting.waitLeisure();
+        Main.overview.hide();
+        await Scripting.waitLeisure();
+
+        System.gc();
+        await Scripting.sleep(1000);
+        Scripting.collectStatistics();
+        Scripting.scriptEvent('afterShowHide');
+    }
+
+    await Scripting.destroyTestWindows();
+    await Scripting.sleep(1000);
+
+    Main.overview.show();
+    await Scripting.waitLeisure();
+
+    for (let i = 0; i < 2; i++) {
+        Scripting.scriptEvent('applicationsShowStart');
+        // eslint-disable-next-line require-atomic-updates
+        Main.overview.dash.showAppsButton.checked = true;
+        await Scripting.waitLeisure();
+        Scripting.scriptEvent('applicationsShowDone');
+        // eslint-disable-next-line require-atomic-updates
+        Main.overview.dash.showAppsButton.checked = false;
+        await Scripting.waitLeisure();
+    }
+    /* eslint-enable no-await-in-loop */
+}
+
+let showingOverview = false;
+let finishedShowingOverview = false;
+let overviewShowStart;
+let overviewFrames;
+let overviewLatency;
+let mallocUsedSize = 0;
+let overviewShowCount = 0;
+let haveSwapComplete = false;
+let applicationsShowStart;
+let applicationsShowCount = 0;
+
+export function script_overviewShowStart(time) {
+    showingOverview = true;
+    finishedShowingOverview = false;
+    overviewShowStart = time;
+    overviewFrames = 0;
+}
+
+export function script_overviewShowDone(_time) {
+    // We've set up the state at the end of the zoom out, but we
+    // need to wait for one more frame to paint before we count
+    // ourselves as done.
+    finishedShowingOverview = true;
+}
+
+export function script_applicationsShowStart(time) {
+    applicationsShowStart = time;
+}
+
+export function script_applicationsShowDone(time) {
+    applicationsShowCount++;
+    if (applicationsShowCount == 1)
+        METRICS.applicationsShowTimeFirst.value = time - applicationsShowStart;
+    else
+        METRICS.applicationsShowTimeSubsequent.value = time - applicationsShowStart;
+}
+
+export function script_afterShowHide(_time) {
+    if (overviewShowCount == 1)
+        METRICS.usedAfterOverview.value = mallocUsedSize;
+    else
+        METRICS.leakedAfterOverview.value = mallocUsedSize - METRICS.usedAfterOverview.value;
+}
+
+export function malloc_usedSize(time, bytes) {
+    mallocUsedSize = bytes;
+}
+
+function _frameDone(time) {
+    if (showingOverview) {
+        if (overviewFrames == 0)
+            overviewLatency = time - overviewShowStart;
+
+        overviewFrames++;
+    }
+
+    if (finishedShowingOverview) {
+        showingOverview = false;
+        finishedShowingOverview = false;
+        overviewShowCount++;
+
+        let dt = (time - (overviewShowStart + overviewLatency)) / 1000000;
+
+        // If we see a start frame and an end frame, that would
+        // be 1 frame for a FPS computation, hence the '- 1'
+        let fps = (overviewFrames - 1) / dt;
+
+        if (overviewShowCount == 1) {
+            METRICS.overviewLatencyFirst.value = overviewLatency;
+            METRICS.overviewFpsFirst.value = fps;
+        } else if (overviewShowCount == 2) {
+            METRICS.overviewLatencySubsequent.value = overviewLatency;
+        }
+
+        // Other than overviewFpsFirst, we collect FPS metrics the second
+        // we show each window configuration. overviewShowCount is 1,2,3...
+        if (overviewShowCount % 2 == 0) {
+            let config = WINDOW_CONFIGS[(overviewShowCount / 2) - 1];
+            METRICS[config.metric].value = fps;
+        }
+    }
+}
+
+export function glx_swapComplete(time, swapTime) {
+    haveSwapComplete = true;
+
+    _frameDone(swapTime);
+}
+
+export function clutter_stagePaintDone(time) {
+    // If we aren't receiving GLXBufferSwapComplete events, then we approximate
+    // the time the user sees a frame with the time we finished doing drawing
+    // commands for the frame. This doesn't take into account the time for
+    // the GPU to finish painting, and the time for waiting for the buffer
+    // swap, but if this are uniform - every frame takes the same time to draw -
+    // then it won't upset our FPS calculation, though the latency value
+    // will be slightly too low.
+
+    if (!haveSwapComplete)
+        _frameDone(time);
+}
diff --git a/tests/shell/headlessStart.js b/tests/shell/headlessStart.js
new file mode 100644 (file)
index 0000000..6f4157b
--- /dev/null
@@ -0,0 +1,107 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* eslint camelcase: ["error", { properties: "never", allow: ["^script_"] }] */
+
+const GLib = imports.gi.GLib;
+const MetaTest = imports.gi.MetaTest;
+const Shell = imports.gi.Shell;
+
+const Main = imports.ui.main;
+const Scripting = imports.ui.scripting;
+
+// This script tests that the shell handles connecting monitors after startup
+// is properly handled.
+
+export var METRICS = {};
+
+let _testMonitor = null;
+
+/**
+ * init:
+ */
+export function init() {
+    global.connect('shutdown', () => {
+        _testMonitor?.destroy();
+    });
+    GLib.timeout_add_seconds(
+        GLib.PRIORITY_LOW, 2,
+        () => {
+            log('Connecting 1280x720 test monitor');
+            _testMonitor = MetaTest.TestMonitor.new(
+                global.context, 1280, 720, 60.0);
+        });
+    Scripting.defineScriptEvent('monitorsChanged', 'Monitors changed');
+
+    const display = global.display;
+    console.assert(display.get_n_monitors() === 0, 'Not running headless');
+
+    const monitorManager = global.backend.get_monitor_manager();
+    const monitorsChangedHandlerId = monitorManager.connect('monitors-changed',
+        () => {
+            console.assert(display.get_n_monitors() === 1,
+                'Expected a single monitor');
+            Scripting.scriptEvent('monitorsChanged');
+            monitorManager.disconnect(monitorsChangedHandlerId);
+        });
+    Shell.PerfLog.get_default().set_enabled(true);
+}
+
+/**
+ * run:
+ */
+export async function run() {
+    /* eslint-disable no-await-in-loop */
+    Scripting.defineScriptEvent('overviewShowDone', 'Overview finished showing');
+    Scripting.defineScriptEvent('overviewHideDone', 'Overview finished hiding');
+
+    Main.overview.connect('shown',
+        () => Scripting.scriptEvent('overviewShowDone'));
+    Main.overview.connect('hidden',
+        () => Scripting.scriptEvent('overviewHideDone'));
+
+    Main.overview.hide();
+    await Scripting.waitLeisure();
+
+    Main.overview.show();
+    await Scripting.waitLeisure();
+
+    /* eslint-enable no-await-in-loop */
+}
+
+let monitorsChanged = false;
+let overviewHidden = false;
+let overviewShown = false;
+
+/**
+ * script_monitorsChanged:
+ */
+export function script_monitorsChanged() {
+    monitorsChanged = true;
+}
+
+/**
+ * script_overviewHideDone:
+ */
+export function script_overviewHideDone() {
+    overviewHidden = true;
+}
+
+/**
+ * script_overviewShowDone:
+ */
+export function script_overviewShowDone() {
+    overviewShown = true;
+}
+
+/**
+ * finish:
+ */
+export function finish() {
+    if (!monitorsChanged)
+        throw new Error('Monitors never changed');
+
+    if (!overviewHidden)
+        throw new Error('Failed to hide overview');
+
+    if (!overviewShown)
+        throw new Error('Failed to show overview');
+}
diff --git a/tests/shell/hwtest.js b/tests/shell/hwtest.js
new file mode 100644 (file)
index 0000000..6e4cd7e
--- /dev/null
@@ -0,0 +1,313 @@
+/* eslint camelcase: ["error", { properties: "never", allow: ["^script_", "^clutter"] }] */
+const Clutter = imports.gi.Clutter;
+const Gio = imports.gi.Gio;
+const Shell = imports.gi.Shell;
+const Main = imports.ui.main;
+const Scripting = imports.ui.scripting;
+
+export var METRICS = {
+    timeToDesktop: {
+        description: 'Time from starting graphical.target to desktop showing',
+        units: 'us',
+    },
+
+    overviewShowTime: {
+        description: 'Time to switch to overview view, first time',
+        units: 'us',
+    },
+
+    applicationsShowTime: {
+        description: 'Time to switch to applications view, first time',
+        units: 'us',
+    },
+
+    mainViewRedrawTime: {
+        description: 'Time to redraw the main view, full screen',
+        units: 'us',
+    },
+
+    overviewRedrawTime: {
+        description: 'Time to redraw the overview, full screen, 5 windows',
+        units: 'us',
+    },
+
+    applicationRedrawTime: {
+        description: 'Time to redraw frame with a maximized application update',
+        units: 'us',
+    },
+
+    geditStartTime: {
+        description: 'Time from gedit launch to window drawn',
+        units: 'us',
+    },
+};
+
+function waitAndDraw(milliseconds) {
+    let cb;
+
+    let timeline = new Clutter.Timeline({ duration: milliseconds });
+    timeline.start();
+
+    timeline.connect('new-frame', (_timeline, _frame) => {
+        global.stage.queue_redraw();
+    });
+
+    timeline.connect('completed', () => {
+        timeline.stop();
+        if (cb)
+            cb();
+    });
+
+    return callback => (cb = callback);
+}
+
+function waitSignal(object, signal) {
+    let cb;
+
+    let id = object.connect(signal, () => {
+        object.disconnect(id);
+        if (cb)
+            cb();
+    });
+
+    return callback => (cb = callback);
+}
+
+function extractBootTimestamp() {
+    const sp = Gio.Subprocess.new([
+        'journalctl', '-b',
+        'MESSAGE_ID=7d4958e842da4a758f6c1cdc7b36dcc5',
+        'UNIT=graphical.target',
+        '-o',
+        'json',
+    ], Gio.SubprocessFlags.STDOUT_PIPE);
+    let result = null;
+
+    let datastream = Gio.DataInputStream.new(sp.get_stdout_pipe());
+    while (true) { // eslint-disable-line no-constant-condition
+        let [line, length_] = datastream.read_line_utf8(null);
+        if (line === null)
+            break;
+
+        let fields = JSON.parse(line);
+        result = Number(fields['__MONOTONIC_TIMESTAMP']);
+    }
+    datastream.close(null);
+    return result;
+}
+
+export async function run() {
+    /* eslint-disable no-await-in-loop */
+    Scripting.defineScriptEvent("desktopShown", "Finished initial animation");
+    Scripting.defineScriptEvent("overviewShowStart", "Starting to show the overview");
+    Scripting.defineScriptEvent("overviewShowDone", "Overview finished showing");
+    Scripting.defineScriptEvent("applicationsShowStart", "Starting to switch to applications view");
+    Scripting.defineScriptEvent("applicationsShowDone", "Done switching to applications view");
+    Scripting.defineScriptEvent("mainViewDrawStart", "Drawing main view");
+    Scripting.defineScriptEvent("mainViewDrawDone", "Ending timing main view drawing");
+    Scripting.defineScriptEvent("overviewDrawStart", "Drawing overview");
+    Scripting.defineScriptEvent("overviewDrawDone", "Ending timing overview drawing");
+    Scripting.defineScriptEvent("redrawTestStart", "Drawing application window");
+    Scripting.defineScriptEvent("redrawTestDone", "Ending timing application window drawing");
+    Scripting.defineScriptEvent("collectTimings", "Accumulate frame timings from redraw tests");
+    Scripting.defineScriptEvent("geditLaunch", "gedit application launch");
+    Scripting.defineScriptEvent("geditFirstFrame", "first frame of gedit window drawn");
+
+    await Scripting.waitLeisure();
+    Scripting.scriptEvent('desktopShown');
+
+    let interfaceSettings = new Gio.Settings({
+        schema_id: 'org.gnome.desktop.interface',
+    });
+    interfaceSettings.set_boolean('enable-animations', false);
+
+    Scripting.scriptEvent('overviewShowStart');
+    Main.overview.show();
+    await Scripting.waitLeisure();
+    Scripting.scriptEvent('overviewShowDone');
+
+    await Scripting.sleep(1000);
+
+    Scripting.scriptEvent('applicationsShowStart');
+    // eslint-disable-next-line require-atomic-updates
+    Main.overview.dash.showAppsButton.checked = true;
+
+    await Scripting.waitLeisure();
+    Scripting.scriptEvent('applicationsShowDone');
+
+    await Scripting.sleep(1000);
+
+    Main.overview.hide();
+    await Scripting.waitLeisure();
+
+    // --------------------- //
+    // Tests of redraw speed //
+    // --------------------- //
+
+    global.frame_timestamps = true;
+    global.frame_finish_timestamp = true;
+
+    for (let k = 0; k < 5; k++)
+        await Scripting.createTestWindow({ maximized: true });
+    await Scripting.waitTestWindows();
+
+    await Scripting.sleep(1000);
+
+    Scripting.scriptEvent('mainViewDrawStart');
+    await waitAndDraw(1000);
+    Scripting.scriptEvent('mainViewDrawDone');
+
+    Main.overview.show();
+    Scripting.waitLeisure();
+
+    await Scripting.sleep(1500);
+
+    Scripting.scriptEvent('overviewDrawStart');
+    await waitAndDraw(1000);
+    Scripting.scriptEvent('overviewDrawDone');
+
+    await Scripting.destroyTestWindows();
+    Main.overview.hide();
+
+    await Scripting.createTestWindow({
+        maximized: true,
+        redraws: true,
+    });
+    await Scripting.waitTestWindows();
+
+    await Scripting.sleep(1000);
+
+    Scripting.scriptEvent('redrawTestStart');
+    await Scripting.sleep(1000);
+    Scripting.scriptEvent('redrawTestDone');
+
+    await Scripting.sleep(1000);
+    Scripting.scriptEvent('collectTimings');
+
+    await Scripting.destroyTestWindows();
+
+    global.frame_timestamps = false;
+    global.frame_finish_timestamp = false;
+
+    await Scripting.sleep(1000);
+
+    let appSys = Shell.AppSystem.get_default();
+    let app = appSys.lookup_app('org.gnome.gedit.desktop');
+
+    Scripting.scriptEvent('geditLaunch');
+    app.activate();
+
+    let windows = app.get_windows();
+    if (windows.length > 0)
+        throw new Error('gedit was already running');
+
+    while (windows.length == 0) {
+        await waitSignal(global.display, 'window-created');
+        windows = app.get_windows();
+    }
+
+    let actor = windows[0].get_compositor_private();
+    await waitSignal(actor, 'first-frame');
+    Scripting.scriptEvent('geditFirstFrame');
+
+    await Scripting.sleep(1000);
+
+    windows[0].delete(global.get_current_time());
+
+    await Scripting.sleep(1000);
+
+    interfaceSettings.set_boolean('enable-animations', true);
+    /* eslint-enable no-await-in-loop */
+}
+
+let overviewShowStart;
+let applicationsShowStart;
+let stagePaintStart;
+let redrawTiming;
+let redrawTimes = {};
+let geditLaunchTime;
+
+export function script_desktopShown(time) {
+    let bootTimestamp = extractBootTimestamp();
+    METRICS.timeToDesktop.value = time - bootTimestamp;
+}
+
+export function script_overviewShowStart(time) {
+    overviewShowStart = time;
+}
+
+export function script_overviewShowDone(time) {
+    METRICS.overviewShowTime.value = time - overviewShowStart;
+}
+
+export function script_applicationsShowStart(time) {
+    applicationsShowStart = time;
+}
+
+export function script_applicationsShowDone(time) {
+    METRICS.applicationsShowTime.value = time - applicationsShowStart;
+}
+
+export function script_mainViewDrawStart(_time) {
+    redrawTiming = 'mainView';
+}
+
+export function script_mainViewDrawDone(_time) {
+    redrawTiming = null;
+}
+
+export function script_overviewDrawStart(_time) {
+    redrawTiming = 'overview';
+}
+
+export function script_overviewDrawDone(_time) {
+    redrawTiming = null;
+}
+
+export function script_redrawTestStart(_time) {
+    redrawTiming = 'application';
+}
+
+export function script_redrawTestDone(_time) {
+    redrawTiming = null;
+}
+
+export function script_collectTimings(_time) {
+    for (let timing in redrawTimes) {
+        let times = redrawTimes[timing];
+        times.sort((a, b) => a - b);
+
+        let len = times.length;
+        let median;
+
+        if (len == 0)
+            median = -1;
+        else if (len % 2 == 1)
+            median = times[(len - 1) / 2];
+        else
+            median = Math.round((times[len / 2 - 1] + times[len / 2]) / 2);
+
+        METRICS[`${timing}RedrawTime`].value = median;
+    }
+}
+
+export function script_geditLaunch(time) {
+    geditLaunchTime = time;
+}
+
+export function script_geditFirstFrame(time) {
+    METRICS.geditStartTime.value = time - geditLaunchTime;
+}
+
+export function clutter_stagePaintStart(time) {
+    stagePaintStart = time;
+}
+
+export function clutter_paintCompletedTimestamp(time) {
+    if (redrawTiming != null && stagePaintStart != null) {
+        if (!(redrawTiming in redrawTimes))
+            redrawTimes[redrawTiming] = [];
+        redrawTimes[redrawTiming].push(time - stagePaintStart);
+    }
+    stagePaintStart = null;
+}